// Copyright (c) Wojciech Figat. All rights reserved.

#if COMPILE_WITH_MODEL_TOOL && USE_ASSIMP

#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Utilities/AnsiPathTempFile.h"

// Import Assimp library
// Source: https://github.com/assimp/assimp
#define ASSIMP_BUILD_NO_EXPORT
#include <ThirdParty/assimp/Importer.hpp>
#include <ThirdParty/assimp/types.h>
#include <ThirdParty/assimp/config.h>
#include <ThirdParty/assimp/scene.h>
#include <ThirdParty/assimp/version.h>
#include <ThirdParty/assimp/postprocess.h>
#include <ThirdParty/assimp/LogStream.hpp>
#include <ThirdParty/assimp/DefaultLogger.hpp>
#include <ThirdParty/assimp/Logger.hpp>

class AssimpLogStream : public Assimp::LogStream
{
public:
    AssimpLogStream()
    {
        Assimp::DefaultLogger::create("");
        Assimp::DefaultLogger::get()->attachStream(this);
    }

    ~AssimpLogStream()
    {
        Assimp::DefaultLogger::get()->detachStream(this);
        Assimp::DefaultLogger::kill();
    }

    void write(const char* message) override
    {
        String s(message);
        if (s.Length() <= 0)
            return;
        for (int32 i = 0; i < s.Length(); i++)
        {
            Char& c = s[i];
            if (c == '\n')
                c = ' ';
            else if (c >= 255)
                c = '?';
        }
        LOG(Info, "[Assimp]: {0}", s);
    }
};

Float2 ToFloat2(const aiVector2D& v)
{
    return Float2(v.x, v.y);
}

Float2 ToFloat2(const aiVector3D& v)
{
    return Float2(v.x, v.y);
}

Float3 ToFloat3(const aiVector3D& v)
{
    return Float3(v.x, v.y, v.z);
}

Color ToColor(const aiColor3D& v)
{
    return Color(v.r, v.g, v.b, 1.0f);
}

Color ToColor(const aiColor4D& v)
{
    return Color(v.r, v.g, v.b, v.a);
}

Quaternion ToQuaternion(const aiQuaternion& v)
{
    return Quaternion(v.x, v.y, v.z, v.w);
}

Matrix ToMatrix(const aiMatrix4x4& mat)
{
    return Matrix(mat.a1, mat.b1, mat.c1, mat.d1,
                  mat.a2, mat.b2, mat.c2, mat.d2,
                  mat.a3, mat.b3, mat.c3, mat.d3,
                  mat.a4, mat.b4, mat.c4, mat.d4);
}

struct AssimpNode
{
    /// <summary>
    /// The parent index. The root node uses value -1.
    /// </summary>
    int32 ParentIndex;

    /// <summary>
    /// The local transformation of the bone, relative to parent bone.
    /// </summary>
    Transform LocalTransform;

    /// <summary>
    /// The name of this bone.
    /// </summary>
    String Name;

    /// <summary>
    /// The LOD index of the data in this node (used to separate meshes across different level of details).
    /// </summary>
    int32 LodIndex;
};

struct AssimpBone
{
    /// <summary>
    /// The index of the related node.
    /// </summary>
    int32 NodeIndex;

    /// <summary>
    /// The parent bone index. The root bone uses value -1.
    /// </summary>
    int32 ParentBoneIndex;

    /// <summary>
    /// The name of this bone.
    /// </summary>
    String Name;

    /// <summary>
    /// The matrix that transforms from mesh space to bone space in bind pose.
    /// </summary>
    Matrix OffsetMatrix;

    bool operator<(const AssimpBone& other) const
    {
        return NodeIndex < other.NodeIndex;
    }
};

struct AssimpImporterData
{
    Assimp::Importer AssimpImporter;
    AssimpLogStream AssimpLogStream;
    const String Path;
    const aiScene* Scene = nullptr;
    const ModelTool::Options& Options;

    Array<AssimpNode> Nodes;
    Array<AssimpBone> Bones;
    Dictionary<int32, Array<int32>> MeshIndexToNodeIndex;

    AssimpImporterData(const StringView& path, const ModelTool::Options& options)
        : Path(path)
        , Options(options)
    {
    }

    int32 FindNode(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
    {
        for (int32 i = 0; i < Nodes.Count(); i++)
        {
            if (Nodes[i].Name.Compare(name, caseSensitivity) == 0)
                return i;
        }
        return -1;
    }

    int32 FindBone(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
    {
        for (int32 i = 0; i < Bones.Count(); i++)
        {
            if (Bones[i].Name.Compare(name, caseSensitivity) == 0)
                return i;
        }
        return -1;
    }

    int32 FindBone(const int32 nodeIndex)
    {
        for (int32 i = 0; i < Bones.Count(); i++)
        {
            if (Bones[i].NodeIndex == nodeIndex)
                return i;
        }
        return -1;
    }
};

void ProcessNodes(AssimpImporterData& data, aiNode* aNode, int32 parentIndex)
{
    const int32 nodeIndex = data.Nodes.Count();

    // Assign the index of the node to the index of the mesh
    for (unsigned i = 0; i < aNode->mNumMeshes; i++)
    {
        int meshIndex = aNode->mMeshes[i];
        data.MeshIndexToNodeIndex[meshIndex].Add(nodeIndex);
    }

    // Create node
    AssimpNode node;
    node.ParentIndex = parentIndex;
    node.Name = aNode->mName.C_Str();

    // Pick node LOD index
    if (parentIndex == -1 || !data.Options.ImportLODs)
    {
        node.LodIndex = 0;
    }
    else
    {
        node.LodIndex = data.Nodes[parentIndex].LodIndex;
        if (node.LodIndex == 0)
        {
            node.LodIndex = ModelTool::DetectLodIndex(node.Name);
        }
        ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
    }

    Matrix transform = ToMatrix(aNode->mTransformation);
    transform.Decompose(node.LocalTransform);
    data.Nodes.Add(node);

    // Process the children
    for (unsigned i = 0; i < aNode->mNumChildren; i++)
    {
        ProcessNodes(data, aNode->mChildren[i], nodeIndex);
    }
}

bool ProcessMesh(ModelData& result, AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, String& errorMsg)
{
    // Properties
    mesh.Name = aMesh->mName.C_Str();
    mesh.MaterialSlotIndex = aMesh->mMaterialIndex;

    // Vertex positions
    mesh.Positions.Set((const Float3*)aMesh->mVertices, aMesh->mNumVertices);

    // Texture coordinates
    for (int32 channelIndex = 0; channelIndex < MODEL_MAX_UV && aMesh->mTextureCoords[channelIndex]; channelIndex++)
    {
        mesh.UVs.Resize(channelIndex + 1);
        auto& channel = mesh.UVs[channelIndex];
        channel.Resize(aMesh->mNumVertices, false);
        aiVector3D* a = aMesh->mTextureCoords[channelIndex];
        for (uint32 v = 0; v < aMesh->mNumVertices; v++)
        {
            channel.Get()[v] = *(Float2*)a;
            a++;
        }
    }

    // Indices
    const int32 indicesCount = aMesh->mNumFaces * 3;
    mesh.Indices.Resize(indicesCount, false);
    for (unsigned faceIndex = 0, i = 0; faceIndex < aMesh->mNumFaces; faceIndex++)
    {
        const auto face = &aMesh->mFaces[faceIndex];
        if (face->mNumIndices != 3)
        {
            errorMsg = TEXT("All faces in a mesh must be triangles!");
            return true;
        }

        mesh.Indices[i++] = face->mIndices[0];
        mesh.Indices[i++] = face->mIndices[1];
        mesh.Indices[i++] = face->mIndices[2];
    }

    // Normals
    if (data.Options.CalculateNormals || !aMesh->mNormals)
    {
        // Support generation of normals when using assimp.
        if (mesh.GenerateNormals(data.Options.SmoothingNormalsAngle))
        {
            errorMsg = TEXT("Failed to generate normals.");
            return true;
        }
    }
    else if (aMesh->mNormals)
    {
        mesh.Normals.Set((const Float3*)aMesh->mNormals, aMesh->mNumVertices);
    }

    // Tangents
    if (aMesh->mTangents)
    {
        mesh.Tangents.Set((const Float3*)aMesh->mTangents, aMesh->mNumVertices);
    }

    // Lightmap UVs
    mesh.SetLightmapUVsSource(data.Options.LightmapUVsSource);

    // Vertex Colors
    if (data.Options.ImportVertexColors && aMesh->mColors[0])
    {
        mesh.Colors.Resize(aMesh->mNumVertices, false);
        aiColor4D* a = aMesh->mColors[0];
        for (uint32 v = 0; v < aMesh->mNumVertices; v++)
        {
            mesh.Colors[v] = *(Color*)a;
            a++;
        }
    }

    // Blend Indices and Blend Weights
    if (aMesh->mNumBones > 0 && aMesh->mBones && EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Skeleton))
    {
        const int32 vertexCount = mesh.Positions.Count();
        mesh.BlendIndices.Resize(vertexCount);
        mesh.BlendWeights.Resize(vertexCount);
        mesh.BlendIndices.SetAll(Int4::Zero);
        mesh.BlendWeights.SetAll(Float4::Zero);

        // Build skinning clusters and fill controls points data structure
        for (unsigned boneId = 0; boneId < aMesh->mNumBones; boneId++)
        {
            const auto aBone = aMesh->mBones[boneId];

            // Find the node where the bone is mapped - based on the name
            const String boneName(aBone->mName.C_Str());
            const int32 nodeIndex = data.FindNode(boneName);
            if (nodeIndex == -1)
            {
                LOG(Warning, "Invalid mesh bone linkage. Mesh: {0}, bone: {1}. Skipping...", mesh.Name, boneName);
                continue;
            }

            // Create bone if missing
            int32 boneIndex = data.FindBone(boneName);
            if (boneIndex == -1)
            {
                // Find the parent bone
                int32 parentBoneIndex = -1;
                for (int32 i = nodeIndex; i != -1; i = data.Nodes[i].ParentIndex)
                {
                    parentBoneIndex = data.FindBone(i);
                    if (parentBoneIndex != -1)
                        break;
                }

                // Add bone
                boneIndex = data.Bones.Count();
                data.Bones.EnsureCapacity(Math::Max(128, boneIndex + 16));
                data.Bones.Resize(boneIndex + 1);
                auto& bone = data.Bones[boneIndex];

                // Setup bone
                bone.Name = boneName;
                bone.NodeIndex = nodeIndex;
                bone.ParentBoneIndex = parentBoneIndex;
                bone.OffsetMatrix = ToMatrix(aBone->mOffsetMatrix);
            }

            // Apply the bone influences
            for (unsigned vtxWeightId = 0; vtxWeightId < aBone->mNumWeights; vtxWeightId++)
            {
                const auto vtxWeight = aBone->mWeights[vtxWeightId];

                if (vtxWeight.mWeight <= 0 || vtxWeight.mVertexId >= (unsigned)vertexCount)
                    continue;

                auto& indices = mesh.BlendIndices[vtxWeight.mVertexId];
                auto& weights = mesh.BlendWeights[vtxWeight.mVertexId];

                for (int32 k = 0; k < 4; k++)
                {
                    if (vtxWeight.mWeight >= weights.Raw[k])
                    {
                        for (int32 l = 2; l >= k; l--)
                        {
                            indices.Raw[l + 1] = indices.Raw[l];
                            weights.Raw[l + 1] = weights.Raw[l];
                        }

                        indices.Raw[k] = boneIndex;
                        weights.Raw[k] = vtxWeight.mWeight;
                        break;
                    }
                }
            }
        }

        mesh.NormalizeBlendWeights();
    }

    // Blend Shapes
    if (aMesh->mNumAnimMeshes > 0 && EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Skeleton) && data.Options.ImportBlendShapes)
    {
        mesh.BlendShapes.EnsureCapacity(aMesh->mNumAnimMeshes);
        for (unsigned int animMeshIndex = 0; animMeshIndex < aMesh->mNumAnimMeshes; animMeshIndex++)
        {
            const aiAnimMesh* aAnimMesh = aMesh->mAnimMeshes[animMeshIndex];

            BlendShape& blendShapeData = mesh.BlendShapes.AddOne();
            blendShapeData.Name = aAnimMesh->mName.C_Str();
            if (blendShapeData.Name.IsEmpty())
                blendShapeData.Name = mesh.Name + TEXT("_blend_shape_") + StringUtils::ToString(animMeshIndex);
            blendShapeData.Weight = aAnimMesh->mWeight;
            blendShapeData.Vertices.Resize(aAnimMesh->mNumVertices);
            for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
                blendShapeData.Vertices[i].VertexIndex = i;

            const aiVector3D* shapeVertices = aAnimMesh->mVertices;
            if (shapeVertices)
            {
                for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
                    blendShapeData.Vertices[i].PositionDelta = ToFloat3(shapeVertices[i]) - mesh.Positions[i];
            }
            else
            {
                for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
                    blendShapeData.Vertices[i].PositionDelta = Float3::Zero;
            }

            const aiVector3D* shapeNormals = aAnimMesh->mNormals;
            if (shapeNormals)
            {
                for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
                    blendShapeData.Vertices[i].NormalDelta = ToFloat3(shapeNormals[i]) - mesh.Normals[i];
            }
            else
            {
                for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
                    blendShapeData.Vertices[i].NormalDelta = Float3::Zero;
            }
        }
    }
    return false;
}

bool ImportTexture(ModelData& result, AssimpImporterData& data, aiString& aFilename, int32& textureIndex, TextureEntry::TypeHint type)
{
    // Find texture file path
    const String filename = String(aFilename.C_Str()).TrimTrailing();
    String path;
    if (ModelTool::FindTexture(data.Path, filename, path))
        return true;

    // Check if already used
    textureIndex = 0;
    while (textureIndex < result.Textures.Count())
    {
        if (result.Textures[textureIndex].FilePath == path)
            return true;
        textureIndex++;
    }

    // Import texture
    auto& texture = result.Textures.AddOne();
    texture.FilePath = path;
    texture.Type = type;
    texture.AssetID = Guid::Empty;
    return true;
}

bool ImportMaterialTexture(ModelData& result, AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type)
{
    aiString aFilename;
    if (aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS)
    {
        // Check for embedded textures
        String filename = String(aFilename.C_Str()).TrimTrailing();
        if (filename.StartsWith(TEXT(AI_EMBEDDED_TEXNAME_PREFIX)))
        {
            const aiTexture* aTex = data.Scene->GetEmbeddedTexture(aFilename.C_Str());
            const StringView texIndexName(filename.Get() + (ARRAY_COUNT(AI_EMBEDDED_TEXNAME_PREFIX) - 1));
            uint32 texIndex;
            if (!aTex && !StringUtils::Parse(texIndexName.Get(), texIndexName.Length(), &texIndex) && texIndex >= 0 && texIndex < data.Scene->mNumTextures)
                aTex = data.Scene->mTextures[texIndex];
            if (aTex && aTex->mHeight == 0 && aTex->mWidth > 0)
            {
                // Export embedded texture to temporary file
                filename = String::Format(TEXT("{0}_tex_{1}.{2}"), StringUtils::GetFileNameWithoutExtension(data.Path), texIndexName, String(aTex->achFormatHint));
                File::WriteAllBytes(String(StringUtils::GetDirectoryName(data.Path)) / filename, (const byte*)aTex->pcData, (int32)aTex->mWidth);
            }
        }

        // Find texture file path
        String path;
        if (ModelTool::FindTexture(data.Path, filename, path))
            return true;

        // Check if already used
        textureIndex = 0;
        while (textureIndex < result.Textures.Count())
        {
            if (result.Textures[textureIndex].FilePath == path)
                return true;
            textureIndex++;
        }

        // Import texture
        auto& texture = result.Textures.AddOne();
        texture.FilePath = path;
        texture.Type = type;
        texture.AssetID = Guid::Empty;
        return true;
    }
    return false;
}

bool ImportMaterials(ModelData& result, AssimpImporterData& data, String& errorMsg)
{
    const uint32 materialsCount = data.Scene->mNumMaterials;
    result.Materials.Resize(materialsCount, false);
    for (uint32 i = 0; i < materialsCount; i++)
    {
        auto& materialSlot = result.Materials[i];
        const aiMaterial* aMaterial = data.Scene->mMaterials[i];

        aiString aName;
        if (aMaterial->Get(AI_MATKEY_NAME, aName) == AI_SUCCESS)
            materialSlot.Name = String(aName.C_Str()).TrimTrailing();
        materialSlot.AssetID = Guid::Empty;

        if (EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Materials))
        {
            aiColor3D aColor;
            if (aMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, aColor) == AI_SUCCESS)
                materialSlot.Diffuse.Color = ToColor(aColor);
            if (aMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, aColor) == AI_SUCCESS)
                materialSlot.Emissive.Color = ToColor(aColor);
            if (aMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, aColor) == AI_SUCCESS)
                materialSlot.Emissive.Color = ToColor(aColor);
            bool aBoolean;
            if (aMaterial->Get(AI_MATKEY_TWOSIDED, aBoolean) == AI_SUCCESS)
                materialSlot.TwoSided = aBoolean;
            if (aMaterial->Get(AI_MATKEY_ENABLE_WIREFRAME, aBoolean) == AI_SUCCESS)
                materialSlot.Wireframe = aBoolean;
            float aFloat;
            if (aMaterial->Get(AI_MATKEY_OPACITY, aFloat) == AI_SUCCESS)
                materialSlot.Opacity.Value = aFloat;
            if (aMaterial->Get(AI_MATKEY_GLOSSINESS_FACTOR, aFloat) == AI_SUCCESS)
                materialSlot.Roughness.Value = 1.0f - aFloat;
            else if (aMaterial->Get(AI_MATKEY_SHININESS, aFloat) == AI_SUCCESS)
                materialSlot.Roughness.Value = MaterialSlotEntry::ShininessToRoughness(aFloat);
            if (aMaterial->Get(AI_MATKEY_EMISSIVE_INTENSITY, aFloat) == AI_SUCCESS)
                materialSlot.Emissive.Color *= aFloat;

            if (EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Textures))
            {
                ImportMaterialTexture(result, data, aMaterial, aiTextureType_DIFFUSE, materialSlot.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB);
                ImportMaterialTexture(result, data, aMaterial, aiTextureType_EMISSIVE, materialSlot.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB);
                ImportMaterialTexture(result, data, aMaterial, aiTextureType_NORMALS, materialSlot.Normals.TextureIndex, TextureEntry::TypeHint::Normals);
                ImportMaterialTexture(result, data, aMaterial, aiTextureType_OPACITY, materialSlot.Opacity.TextureIndex, TextureEntry::TypeHint::ColorRGBA);
                ImportMaterialTexture(result, data, aMaterial, aiTextureType_METALNESS, materialSlot.Metalness.TextureIndex, TextureEntry::TypeHint::ColorRGB);
                ImportMaterialTexture(result, data, aMaterial, aiTextureType_DIFFUSE_ROUGHNESS, materialSlot.Roughness.TextureIndex, TextureEntry::TypeHint::ColorRGB);

                if (materialSlot.Roughness.TextureIndex != -1 && (data.Path.EndsWith(TEXT(".gltf")) || data.Path.EndsWith(TEXT(".glb"))))
                {
                    // glTF specification with a single metallicRoughnessTexture (G = roughness, B = metalness)
                    materialSlot.Roughness.Channel = 1;
                    materialSlot.Metalness.Channel = 2;
                }

                if (materialSlot.Diffuse.TextureIndex != -1)
                {
                    // Detect using alpha mask in diffuse texture
                    materialSlot.Diffuse.HasAlphaMask = TextureTool::HasAlpha(result.Textures[materialSlot.Diffuse.TextureIndex].FilePath);
                    if (materialSlot.Diffuse.HasAlphaMask)
                        result.Textures[materialSlot.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA;
                }
            }
        }
    }

    return false;
}

bool IsMeshInvalid(const aiMesh* aMesh)
{
    return aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3;
}

bool ImportMesh(int32 index, ModelData& result, AssimpImporterData& data, String& errorMsg)
{
    const auto aMesh = data.Scene->mMeshes[index];

    // Skip invalid meshes
    if (IsMeshInvalid(aMesh))
        return false;

    // Skip unused meshes
    if (!data.MeshIndexToNodeIndex.ContainsKey(index))
        return false;

    // Import mesh data
    MeshData* meshData = New<MeshData>();
    if (ProcessMesh(result, data, aMesh, *meshData, errorMsg))
    {
        Delete(meshData);
        return true;
    }

    auto& nodesWithMesh = data.MeshIndexToNodeIndex[index];
    for (int32 i = 0; i < nodesWithMesh.Count(); i++)
    {
        const auto nodeIndex = nodesWithMesh[i];
        auto& node = data.Nodes[nodeIndex];
        const int32 lodIndex = node.LodIndex;

        // The first mesh instance uses meshData directly while others have to clone it
        if (i != 0)
        {
            meshData = New<MeshData>(*meshData);
        }

        // Link mesh
        meshData->NodeIndex = nodeIndex;
        AssimpNode* curNode = &data.Nodes[meshData->NodeIndex];
        
        meshData->OriginTranslation = curNode->LocalTransform.Translation;
        meshData->OriginOrientation = curNode->LocalTransform.Orientation;
        meshData->Scaling = curNode->LocalTransform.Scale;

        if (result.LODs.Count() <= lodIndex)
            result.LODs.Resize(lodIndex + 1);
        result.LODs[lodIndex].Meshes.Add(meshData);
    }

    return false;
}

void ImportCurve(aiVectorKey* keys, uint32 keysCount, LinearCurve<Float3>& curve)
{
    if (keys == nullptr || keysCount == 0)
        return;

    const auto keyframes = curve.Resize(keysCount);

    for (uint32 i = 0; i < keysCount; i++)
    {
        auto& aKey = keys[i];
        auto& key = keyframes[i];

        key.Time = (float)aKey.mTime;
        key.Value = ToFloat3(aKey.mValue);
    }
}

void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve<Quaternion>& curve)
{
    if (keys == nullptr || keysCount == 0)
        return;

    const auto keyframes = curve.Resize(keysCount);

    for (uint32 i = 0; i < keysCount; i++)
    {
        auto& aKey = keys[i];
        auto& key = keyframes[i];

        key.Time = (float)aKey.mTime;
        key.Value = ToQuaternion(aKey.mValue);
    }
}

void ImportAnimation(int32 index, ModelData& data, AssimpImporterData& importerData)
{
    const auto animations = importerData.Scene->mAnimations[index];
    auto& anim = data.Animations.AddOne();
    anim.Channels.Resize(animations->mNumChannels, false);
    anim.Duration = animations->mDuration;
    anim.FramesPerSecond = animations->mTicksPerSecond;
    if (anim.FramesPerSecond <= 0)
    {
        anim.FramesPerSecond = importerData.Options.DefaultFrameRate;
        if (anim.FramesPerSecond <= 0)
            anim.FramesPerSecond = 30.0f;
    }
    anim.Name = animations->mName.C_Str();

    for (unsigned i = 0; i < animations->mNumChannels; i++)
    {
        const auto aAnim = animations->mChannels[i];
        auto& channel = anim.Channels[i];

        channel.NodeName = aAnim->mNodeName.C_Str();

        ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, channel.Position);
        ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, channel.Rotation);
        if (importerData.Options.ImportScaleTracks)
            ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, channel.Scale);
    }
}

bool ModelTool::ImportDataAssimp(const String& path, ModelData& data, Options& options, String& errorMsg)
{
    static bool AssimpInited = false;
    if (!AssimpInited)
    {
        AssimpInited = true;
        LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision());
    }
    bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry);
    bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations);
    AssimpImporterData context(path, options);

    // Setup import flags
    unsigned int flags =
            aiProcess_JoinIdenticalVertices |
            aiProcess_LimitBoneWeights |
            aiProcess_Triangulate |
            aiProcess_SortByPType |
            aiProcess_GenUVCoords |
            aiProcess_FindDegenerates |
            aiProcess_FindInvalidData |
            aiProcess_GlobalScale |
            //aiProcess_ValidateDataStructure |
            aiProcess_ConvertToLeftHanded;
    if (importMeshes)
    {
        if (options.CalculateNormals)
            flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals;
        if (options.CalculateTangents)
            flags |= aiProcess_CalcTangentSpace;
        if (options.ReverseWindingOrder)
            flags &= ~aiProcess_FlipWindingOrder;
        if (options.OptimizeMeshes)
            flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality;
        if (options.MergeMeshes)
            flags |= aiProcess_RemoveRedundantMaterials;
    }

    // Setup import options
    context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle);
    context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle);
    context.AssimpImporter.SetPropertyFloat(AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY, 100.0f); // Convert to cm
    //context.AssimpImporter.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16);
    context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
    context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
    context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false);
    context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations);
    //context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed
    context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);

    // Import file
    {
        AnsiPathTempFile tempFile(path);
        context.Scene = context.AssimpImporter.ReadFile(tempFile.Path.Get(), flags);
    }
    if (context.Scene == nullptr)
    {
        LOG_STR(Warning, String(context.AssimpImporter.GetErrorString()));
        LOG_STR(Warning, String(path));
        LOG_STR(Warning, StringUtils::ToString(flags));
        errorMsg = context.AssimpImporter.GetErrorString();
        return true;
    }

    // Create root node
    /*AssimpNode& rootNode = context.Nodes.AddOne();
    rootNode.ParentIndex = -1;
    rootNode.LodIndex = 0;
    rootNode.LocalTransform = Transform::Identity;
    rootNode.Name = TEXT("Root");*/

    // Process imported scene nodes
    ProcessNodes(context, context.Scene->mRootNode, -1);

    // Import materials
    if (ImportMaterials(data, context, errorMsg))
    {
        LOG(Warning, "Failed to import materials.");
        return true;
    }

    // Import geometry
    if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context.Scene->HasMeshes())
    {
        for (unsigned meshIndex = 0; meshIndex < context.Scene->mNumMeshes; meshIndex++)
        {
            if (ImportMesh((int32)meshIndex, data, context, errorMsg))
                return true;
        }
    }

    // Import skeleton
    if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
    {
        data.Skeleton.Nodes.Resize(context.Nodes.Count(), false);
        for (int32 i = 0; i < context.Nodes.Count(); i++)
        {
            auto& node = data.Skeleton.Nodes[i];
            auto& aNode = context.Nodes[i];

            node.Name = aNode.Name;
            node.ParentIndex = aNode.ParentIndex;
            node.LocalTransform = aNode.LocalTransform;
        }

        data.Skeleton.Bones.Resize(context.Bones.Count(), false);
        for (int32 i = 0; i < context.Bones.Count(); i++)
        {
            auto& bone = data.Skeleton.Bones[i];
            auto& aBone = context.Bones[i];

            const auto boneNodeIndex = aBone.NodeIndex;
            const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context.Bones[aBone.ParentBoneIndex].NodeIndex;

            bone.ParentIndex = aBone.ParentBoneIndex;
            bone.NodeIndex = aBone.NodeIndex;
            bone.LocalTransform = CombineTransformsFromNodeIndices(context.Nodes, parentBoneNodeIndex, boneNodeIndex);
            bone.OffsetMatrix = aBone.OffsetMatrix;
        }
    }

    // Import animations
    if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations))
    {
        for (unsigned animIndex = 0; animIndex < context.Scene->mNumAnimations; animIndex++)
        {
            ImportAnimation((int32)animIndex, data, context);
        }
    }

    // Import nodes
    if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes))
    {
        data.Nodes.Resize(context.Nodes.Count());
        for (int32 i = 0; i < context.Nodes.Count(); i++)
        {
            auto& node = data.Nodes[i];
            auto& aNode = context.Nodes[i];

            node.Name = aNode.Name;
            node.ParentIndex = aNode.ParentIndex;
            node.LocalTransform = aNode.LocalTransform;
        }
    }

    return false;
}

#endif
